/**
 * JSSDK 服务端，运行在 QunTool 网页中
 */
import { getLogger, success, failed } from '@gnlab/utils'
import pick from 'lodash/pick'
import services from '@/services'
import { type Account } from '@/services/auth'
import { ChatRecordType } from '@/services/contact-chat'
import type { Contact } from '@/services/contacts'
import { ContactType, makeContactId } from '@/services/contacts'
import { getQywxs } from '@/services/qywxs'
import { type MiniProgram } from '@/services/resources/mini-programs'

const logger = getLogger('jssdk-host')

export interface JSSDKMessgae<T = unknown> {
  quntoolJSSDK: true
  type: string
  data: T
}

// handler 返回的值会作为”反馈消息“发回给 client
// handler 可以返回 Promise 以异步完成任务并异步推送“反馈消息”
type HostEventHandler<T, R> = (data: T) => R

export type SendItem =
  | { type: 'text'; data: string }
  | { type: 'image'; data: string }
  | { type: 'file'; data: string }
  | { type: 'video'; data: string }
  | { type: 'link'; data: { title: string; desc?: string; url: string; imageUrl?: string } }
  | { type: 'miniprogram'; data: number }
  | { type: 'videonum'; data: number }

export type AccountForSDK = Pick<
  Account,
  'name' | 'avatar' | 'isAdmin' | 'isSuperAdmin' | 'token'
> & {
  id: number
  parentId: number | null
}

export function appendSDKQuery(url: string) {
  const query = 'in-quntool=1'
  if (url.includes('?')) return url.replace('?', `?${query}&`)
  if (url.includes('#')) return url.replace('#', `?${query}#`)
  return `${url}?${query}`
}

export default class SDKHost {
  constructor() {
    logger.debug('初始化')
    this.handleMessage = this.handleMessage.bind(this)
    window.addEventListener('message', this.handleMessage) // eslint-disable-line @typescript-eslint/unbound-method

    this.registerConnectTest()
    this.registerGetTime()
    this.registerGetIsObserving()
    this.registerGetAccount()
    this.registerGetQywxList()
    this.registerGetCurrentContact()
    this.registerGetCustomerInfo()
    this.registerSendText()
    this.registerSendImage()
    this.registerSendFile()
    this.registerSendVideo()
    this.registerSendLink()
    this.registerLoadMiniprograms()
    this.registerSendMiniprogram()
    this.registerSendMiniprogramByData()
    this.registerLoadVideonums()
    this.registerSendVideonum()
    this.registerBatchSend()
    this.registerSwitchContact()
  }

  destroy() {
    window.removeEventListener('message', this.handleMessage) // eslint-disable-line @typescript-eslint/unbound-method
  }

  protected outsideListeners = new Map<string, (data: unknown) => void>()
  on<T>(event: string, callback: (data: T) => void) {
    this.outsideListeners.set(event, callback as (data: unknown) => void)
    return () => {
      this.outsideListeners.delete(event)
    }
  }
  trigger(event: string, data: unknown) {
    const listener = this.outsideListeners.get(event)
    if (listener) listener(data)
  }

  protected clientEvents = new Map<string, HostEventHandler<unknown, unknown>>()
  protected registerClientEvent<T, R>(type: string, callback: HostEventHandler<T, R>) {
    this.clientEvents.set(type, callback as (data: unknown) => void)
  }

  protected async handleMessage(message: MessageEvent<JSSDKMessgae>) {
    const raw = message.data
    if (typeof raw !== 'object' || !('quntoolJSSDK' in raw)) return
    const { type, data } = raw
    logger.debug('收到消息：', raw)

    if (!this.clientEvents.has(type)) return void logger.warn(`不认识的消息类型：${type}`, raw)
    const response = await this.clientEvents.get(type)!(data)

    if (message.source) this.sendMessage(message.source as Window, type, response) // 发送反馈消息
    else logger.warn('找不到消息来源窗口，无法发送反馈消息', message)
  }

  protected sendMessage<T>(targetWindow: Window, type: string, data?: T) {
    targetWindow.postMessage({ type, data, quntoolJSSDK: true } as JSSDKMessgae<T>, '*')
  }

  /**
   *     <-- connect-test      客户端发起连接测试
   * connect-test -->          服务端回应消息
   *     <-- connect-confirm   客户端收到回应消息，通知服务端
   * connect-confirm -->       服务端回应消息
   */
  protected registerConnectTest() {
    this.registerClientEvent('connect-test', () => logger.debug('开始连接测试...'))
    this.registerClientEvent('connect-confirm', () => logger.debug('连接测试通过'))
  }

  /**
   * 判断当前是否处于观察者模式
   */
  protected get isObserving() {
    return !!services.auth.observing.in$.value
  }

  /**
   * 一个测试用的命令，返回当前时间
   */
  protected registerGetTime() {
    this.registerClientEvent('get-time', () => new Date().getTime())
  }

  protected registerGetIsObserving() {
    this.registerClientEvent('get-is-observing', this.getIsObserving.bind(this))
  }
  getIsObserving() {
    return success(this.isObserving)
  }

  protected registerGetAccount() {
    this.registerClientEvent('get-account', this.getAccount.bind(this))
  }
  getAccount() {
    const raw = services.auth.account$.value
    if (raw) {
      return success({
        ...pick(raw, 'name', 'avatar', 'isAdmin', 'isSuperAdmin', 'token'),
        id: raw.rawId,
        parentId: raw.rawParentId,
      })
    } else {
      return failed('未能取得账号信息')
    }
  }

  protected registerGetQywxList() {
    this.registerClientEvent('get-qywx-list', this.getQywxList.bind(this))
  }
  async getQywxList() {
    const res = await getQywxs('')
    const accountIds = await services.auth.bizAccountIds
    if (res.success) {
      return success(
        res.data
          .filter(data => accountIds.includes(data.user_id))
          .map(data => ({
            wxid: data.wx_id,
            acctid: data.acctid,
            avatar: data.avatar,
            mobile: data.mobile,
            nickname: data.nickname,
            username: data.username,
            corpid: data.corp_id,
            corp_name: data.corp_name,
            corp_short_name: data.corp_short_name,
            job_name: data.job_name,
            position: data.position,
          })),
      )
    } else {
      return res
    }
  }

  protected registerGetCurrentContact() {
    this.registerClientEvent('get-current-contact', async () => {
      const current: Contact | null = services.contacts.current.contact.value
      if (!current) return failed('没有当前联系人')

      return Promise.resolve(
        success({
          type: current.type,
          wxid: current.ownerWxid,
          contactUserId: current.userId,
          nickname: current.name,
        }),
      )
    })
  }

  protected registerGetCustomerInfo() {
    this.registerClientEvent('get-customer-info', async () => {
      const current: Contact | null = services.contacts.current.contact.value
      if (!current || current.type !== ContactType.external) return failed('当前联系人无效')

      const res = await services.externals.getRelation({
        ownerWxid: current.ownerWxid,
        contactUserId: current.userId,
      })
      if (!res.success) return failed('获取联系人信息失败')

      const relation = res.data
      return success({
        contactUserId: relation.userId,
        externalUserId: relation.externalUserId,
        corpId: relation.corpId,
        corpIdOrigin: relation.corpIdOrigin,
        unionId: relation.unionId,
      })
    })
  }

  protected registerSendText() {
    this.registerClientEvent('send-text', this.sendText.bind(this))
  }
  sendText(text: string) {
    void services.chat.send(ChatRecordType.Text, { atList: [], text })
  }

  protected registerSendImage() {
    this.registerClientEvent('send-image', this.sendImage.bind(this))
  }
  sendImage(url: string) {
    void services.chat.send(ChatRecordType.Image, { url })
  }

  protected registerSendVideo() {
    this.registerClientEvent('send-video', this.sendVideo.bind(this))
  }
  sendVideo(url: string) {
    void services.chat.send(ChatRecordType.Video, { url })
  }

  protected registerSendFile() {
    this.registerClientEvent('send-file', this.sendFile.bind(this))
  }
  sendFile(url: string) {
    const nodes = url.split('/')
    const name = (nodes[nodes.length - 1] ?? '') || url
    void services.chat.send(ChatRecordType.File, { url, name, size: 0 })
  }

  protected registerSendLink() {
    this.registerClientEvent('send-link', this.sendLink.bind(this))
  }
  sendLink(link: { title: string; desc?: string; url: string; imageUrl?: string }) {
    void services.chat.send(ChatRecordType.LinkCard, {
      title: link.title,
      desc: link.desc ?? '',
      url: link.url,
      image: link.imageUrl ?? '',
    })
  }

  protected registerLoadMiniprograms() {
    this.registerClientEvent('load-miniprograms', () => {
      return services.resources.miniPrograms.list.value.map(mp => ({
        id: mp.id,
        title: mp.title,
        appicon: mp.appicon,
      }))
    })
  }

  protected registerSendMiniprogram() {
    this.registerClientEvent('send-miniprogram', this.sendMiniProgram.bind(this))
  }
  async sendMiniProgram(id: number) {
    const mp = services.resources.miniPrograms.list.value.find(m => m.id === id)
    if (!mp) return failed('找不到此小程序')
    await services.chat.send(ChatRecordType.MiniProgram, mp)
    return { success: true }
  }

  protected registerSendMiniprogramByData() {
    this.registerClientEvent('send-miniprogram-by-data', this.sendMiniProgramByData.bind(this))
  }
  async sendMiniProgramByData(data: MiniProgram) {
    await services.chat.send(ChatRecordType.MiniProgram, data)
    return { success: true }
  }

  protected registerLoadVideonums() {
    this.registerClientEvent('load-videonums', () => {
      return services.resources.videonums.list.value.map(videonum => ({
        id: videonum.id,
        title: videonum.desc,
        logo_url: videonum.avatar,
        image_url: videonum.cover_url,
      }))
    })
  }

  protected registerSendVideonum() {
    this.registerClientEvent('send-videonum', this.sendVideonum.bind(this))
  }
  async sendVideonum(id: number) {
    const videonum = services.resources.videonums.list.value.find(v => v.id === id)
    if (!videonum) return failed('找不到此视频号')

    await services.chat.send(ChatRecordType.Videonum, videonum)
    return { success: true }
  }

  protected registerBatchSend() {
    this.registerClientEvent('batch-send', this.batchSendEditing.bind(this))
  }
  batchSendEditing(items: SendItem[]) {
    this.trigger('batch-send-editing', items)
  }
  batchSend(items: SendItem[]) {
    const methods = {
      text: this.sendText.bind(this),
      image: this.sendImage.bind(this),
      file: this.sendFile.bind(this),
      video: this.sendVideo.bind(this),
      link: this.sendLink.bind(this),
      miniprogram: this.sendMiniProgram.bind(this),
      videonum: this.sendVideonum.bind(this),
    }
    for (const item of items) {
      const method = methods[item.type] as (data: unknown) => unknown
      method(item.data)
    }
  }

  protected registerSwitchContact() {
    this.registerClientEvent('switch-contact', this.switchContact.bind(this))
  }
  async switchContact(search: { wxid: string; externalUserId: string }) {
    const { wxid, externalUserId } = search

    const res = await services.externals.getRelation({ ownerWxid: wxid, externalUserId })
    if (!res.success) return failed('寻找联系人失败')

    const contactId = makeContactId(wxid, res.data.userId)
    if (!services.externals.getById(contactId)) {
      const loaded = await services.externals.tryLoadInfo({ id: contactId })
      if (loaded !== true) return failed('找不到此联系人')
    }
    const switched = services.contacts.current.mark(contactId)
    if (!switched) return failed('切换联系人失败')
    return { success: true }
  }
}
